Skip to main content

Ballot

The PREDA voting contract is quite complex, but shows many PREDA's features, e.g., relay transactions and scopes. The PREDA voting contract is functionally equivalent to the Solidity voting contract, but is designed to execute a large number of voting transactions in parallel on a sharding blockchain. The main ideas of the PREDA voting contract are first to allow voters to vote in different shards based on address-to-shard mapping and then to accumulate the voting results of each shard to the final voting result. The PREDA voting contract can avoid reading and writing the shared contract state, i.e., the final voting result, when processing the voting transactions issued by voters. In this case, we show how voting is implemented in each shard and how the final voting result is accumulated.

contract Ballot {

struct Proposal {
string name;
uint64 totalVotedWeight;
}

struct BallotResult {
string topVoted;
uint32 case;
}

@global address controller;
@global uint32 current_case;
@global array<Proposal> proposals;
@global BallotResult last_result;

@global uint32 shardGatherRatio;
@global function shardGather_reset(){ shardGatherRatio = 0u; }
@global function bool shardGather_isCompleted(){ return shardGatherRatio == 0x80000000u; }
@global function bool shardGather_gather()
{ shardGatherRatio += 0x80000000u>>__block.get_shard_order();
return shardGatherRatio == 0x80000000u;
}

@shard array<uint64> votedWeights;

// address scope
@address uint64 weight;
@address uint32 voted_case;

@address function bool is_voting()
{
return last_result.case < current_case;
}

@address function init(array<string> names) export {
//__debug.assert(controller == __transaction.get_self_address());
__debug.assert(!is_voting());
relay@global (^names){
__debug.print("global: ", names);
for (uint32 i = 0u; i < names.length(); i++) {
Proposal proposal;
proposal.name = names[i];
proposal.totalVotedWeight = 0u64;
proposals.push(proposal);
}
current_case++;
last_result.case = 0u;
last_result.topVoted = "";
}
__debug.print("EOC init: ", names);
}

@address function bool vote(uint32 proposal_index, uint32 case_num) export {
if(case_num == current_case && case_num > voted_case && proposal_index<proposals.length())
{
voted_case = case_num;
__debug.print("Vote: ", proposal_index);
votedWeights.set_length(proposals.length());
votedWeights[proposal_index] += weight;
return true;
}

__debug.print("Vote: ", proposal_index, " failed");
return false;
}

@address function finalize() export {
//__debug.assert(controller == __transaction.get_self_address());
__debug.assert(is_voting());
relay@global (){
// ... maybe do something else before scatter-gathering
__debug.print("In global");
shardGather_reset();
relay@shards (){
// ... maybe do something in each shard
__debug.print("Shard Vote: ", votedWeights);
relay@global(auto shardVotes = votedWeights) {
//BEGIN: code for scattering
for(uint32 i=0u; i<shardVotes.length(); i++)
{
proposals[i].totalVotedWeight += uint64(shardVotes[i]);
}
//END

if(shardGather_gather())
{
__debug.print("votes: ", proposals);
//BEGIN: code for gathering
last_result.case = current_case;
uint64 w = 0u64;
for(uint32 i=0u; i<proposals.length(); i++)
{
if(proposals[i].totalVotedWeight > w)
{
last_result.topVoted = proposals[i].name;
w = proposals[i].totalVotedWeight;
}
}

__debug.print("result: ", last_result);
//END
}
}
}
}
}
}


In the address scope function init(), the controller of the contract is responsible for initiating the global scope variable proposals. This is done by issuing a relay transaction from the address scope to the global scope, which is corresponding to broadcast a relay transaction to the network in the underlying blockchain, and doing the proposals and last_result initalizations. After executing the init() function, the global scope variable proposals is initialized on all blockchain nodes.

In the address scope function vote(), each voter can directly read the global scope variable proposals and also write the shard scope variable voteWeights. The global scope variable is globally consistent, while the shard scope variable is consistent in each shard. At runtime, user-issued transactions that call the vote() function are executed by different shards based on address-to-shard mapping, and each transaction only changes the shard scope variable voteWeights in the corresponding shard.

In the address scope function finalize(), the controller of the contract issues a relay transaction from the address scope to the global scope to reset the global scope variable shardGatherRatio and request all shards to report the shard scope variable voteWeights. Each shard then sends a relay transaction with the value of voteWeights to the global. At the global scope of each blockchain node, the partial voting results voteWeights are accumulated to the final voting result proposals. After receiving the partial voting results from all shards in the conditional sentence of calling shardGather_gather(), the winner of the voting proposals is calculated as last_result.